/**
 * Copyright 2018 jianggujin (www.jianggujin.com).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jianggujin.modulelink.mvc;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.jianggujin.modulelink.JModuleContext;
import com.jianggujin.modulelink.mvc.converter.JTypeConverter;
import com.jianggujin.modulelink.mvc.render.JErrorRender;
import com.jianggujin.modulelink.mvc.render.JErrorRender.JErrorRenderParam;
import com.jianggujin.modulelink.mvc.render.JRedirectRender.JRedirectRenderParam;
import com.jianggujin.modulelink.mvc.render.JRenderManager;
import com.jianggujin.modulelink.mvc.resolver.JRequestResolverManager;
import com.jianggujin.modulelink.mvc.upload.JMultipartFile;
import com.jianggujin.modulelink.mvc.upload.JMultipartParsingResult;
import com.jianggujin.modulelink.mvc.upload.JMultipartResolverConfig;
import com.jianggujin.modulelink.mvc.upload.JMultipartResolverFactory;
import com.jianggujin.modulelink.mvc.util.JHttpServletRequestWrapper;
import com.jianggujin.modulelink.mvc.util.JIOUtils;
import com.jianggujin.modulelink.mvc.util.JInjector;
import com.jianggujin.modulelink.mvc.util.JWebUtils;
import com.jianggujin.modulelink.util.JAssert;
import com.jianggujin.modulelink.util.JDefaultMultiValueMap;
import com.jianggujin.modulelink.util.JMultiValueMap;
import com.jianggujin.modulelink.util.JStringUtils;

/**
 * 模块的执行者上下文默认实现
 * 
 * @author jianggujin
 *
 */
public class JActionContextImpl implements JActionContext {
    private final JModuleContext context;
    private final JHttpServletRequestWrapper request;
    private final HttpServletResponse response;
    private volatile JMultiValueMap<String, JMultipartFile> multipartFiles;
    private volatile String[] pathParas = null;

    public JActionContextImpl(JModuleContext context) {
        this.context = context;
        Object in = context.getIn();
        JAssert.checkArgument(in instanceof JActionContext, "in must instanceof JActionContext");
        HttpServletRequest request = null;
        HttpServletResponse response = null;
        this.request = (request instanceof JHttpServletRequestWrapper) ? (JHttpServletRequestWrapper) request
                : new JHttpServletRequestWrapper(request);
        this.response = response;
    }

    @Override
    public String getPara(String name) {
        return this.request.getParameter(name);
    }

    @Override
    public String getPara(String name, String defaultValue) {
        String result = this.request.getParameter(name);
        return result != null && !"".equals(result) ? result : defaultValue;
    }

    @Override
    public String[] getParaValues(String name) {
        return this.request.getParameterValues(name);
    }

    @Override
    public Integer getParaToInt(String name) {
        return getPara(name, Integer.class);
    }

    @Override
    public Integer getParaToInt(String name, Integer defaultValue) {
        return getPara(name, Integer.class, defaultValue);
    }

    @Override
    public Long getParaToLong(String name) {
        return getPara(name, Long.class);
    }

    @Override
    public Long getParaToLong(String name, Long defaultValue) {
        return getPara(name, Long.class, defaultValue);
    }

    @Override
    public Boolean getParaToBoolean(String name) {
        return getPara(name, Boolean.class);
    }

    @Override
    public Boolean getParaToBoolean(String name, Boolean defaultValue) {
        return getPara(name, Boolean.class, defaultValue);
    }

    @Override
    public Date getParaToDate(String name) {
        return getPara(name, Date.class);
    }

    @Override
    public Date getParaToDate(String name, Date defaultValue) {
        return getPara(name, Date.class, defaultValue);
    }

    @Override
    public <T> T getParaToBean(Class<T> clazz) {
        return JInjector.injectBean(clazz, request, false);
    }

    @Override
    public <T> T getParaToBean(Class<T> clazz, boolean skipConvertError) {
        return JInjector.injectBean(clazz, request, skipConvertError);
    }

    @Override
    public <T> T getPara(String name, Class<T> clazz) {
        return toTarget(this.request.getParameter(name), clazz, null);
    }

    @Override
    public <T> T getPara(String name, Class<T> clazz, T defaultValue) {
        return toTarget(this.request.getParameter(name), clazz, defaultValue);
    }

    @Override
    public Map<String, ?> getParaMap() {
        return request.getParameterMap();
    }

    @Override
    public Enumeration<String> getParaNames() {
        return request.getParameterNames();
    }

    @Override
    public void setParaMap(Map<String, ?> paraMap) {
        request.setParameterMap(paraMap);
    }

    @Override
    public JActionContext keepPara() {
        Map<String, ?> map = request.getParameterMap();
        for (Entry<String, ?> e : map.entrySet()) {
            Object values = e.getValue();
            if (values instanceof String) {
                request.setAttribute(e.getKey(), values);
            }
            String[] tmp = request.getParameterValues(e.getKey());
            if (tmp.length == 1)
                request.setAttribute(e.getKey(), tmp[0]);
            else
                request.setAttribute(e.getKey(), tmp);
        }
        return this;
    }

    @Override
    public JActionContext keepPara(String... names) {
        for (String name : names) {
            String[] values = request.getParameterValues(name);
            if (values != null) {
                if (values.length == 1)
                    request.setAttribute(name, values[0]);
                else
                    request.setAttribute(name, values);
            }
        }
        return this;
    }

    @Override
    public boolean isParaExists(String paraName) {
        return request.getParameterMap().containsKey(paraName);
    }

    private void ensureParsePathPara() {
        if (this.pathParas == null) {
            synchronized (this) {
                if (this.pathParas == null) {
                    String context = request.getContextPath();
                    int beginIndex = "/".equals(context) ? 1 : context.length() + 1;
                    this.pathParas = JStringUtils.separateString(this.request.getRequestURI().substring(beginIndex),
                            "/");
                }
            }
        }
    }

    @Override
    public String[] getPathParas() {
        ensureParsePathPara();
        return this.pathParas;
    }

    @Override
    public int getPathCount() {
        return this.getPathParas().length;
    }

    @Override
    public String getPathPara(int pos) {
        String[] paths = getPathParas();
        JAssert.checkPositionIndex(pos, paths.length - 1, "pos");
        return paths[pos];
    }

    @Override
    public Integer getPathParaToInt(int pos) {
        return getPathPara(pos, Integer.class);
    }

    @Override
    public Long getPathParaToLong(int pos) {
        return getPathPara(pos, Long.class);
    }

    @Override
    public Boolean getPathParaToBoolean(int pos) {
        return getPathPara(pos, Boolean.class);
    }

    @Override
    public <T> T getPathPara(int pos, Class<T> clazz) {
        return toTarget(getPathPara(pos), clazz);
    }

    /**
     * 将字符串转换为指定数据类型
     * 
     * @param value
     * @param clazz
     * @param defaultValue
     * @return
     */
    @SuppressWarnings("unchecked")
    private static <T> T toTarget(String value, Class<T> clazz, T defaultValue) {
        try {
            if (JStringUtils.isBlank(value) || clazz == null) {
                return defaultValue;
            }
            return (T) JTypeConverter.getInstance().convert(clazz, value);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * 将字符串转换为指定数据类型
     * 
     * @param value
     * @param clazz
     * @param defaultValue
     * @return
     */
    @SuppressWarnings("unchecked")
    private static <T> T toTarget(String value, Class<T> clazz) {
        if (JStringUtils.isBlank(value) || clazz == null) {
            throw new IllegalArgumentException("value or clazz must not be null.");
        }
        return (T) JTypeConverter.getInstance().convert(clazz, value);
    }

    @Override
    public String getCookie(String name, String defaultValue) {
        Cookie cookie = JWebUtils.getCookie(request, name);
        return cookie != null ? cookie.getValue() : defaultValue;
    }

    @Override
    public String getCookie(String name) {
        return getCookie(name, null);
    }

    @Override
    public JActionContext setCookie(Cookie cookie) {
        response.addCookie(cookie);
        return this;
    }

    @Override
    public JActionContext removeCookie(String name) {
        JWebUtils.doSetCookie(response, name, null, 0, null, null, null);
        return this;
    }

    @Override
    public JActionContext setAttr(String name, Object value) {
        JWebUtils.setAttribute(request, name, value);
        return this;
    }

    @Override
    public Object getAttr(String name) {
        return JWebUtils.getAttribute(request, name);
    }

    @Override
    public JActionContext setSessionAttr(String name, Object value) {
        JWebUtils.setSessionAttribute(request, name, value);
        return this;
    }

    @Override
    public Object getSessionAttr(String name) {
        return JWebUtils.getSessionAttribute(request, name);
    }

    @Override
    public JActionContext setContextAttr(String name, Object value) {
        JWebUtils.setContextAttribute(request, name, value);
        return this;
    }

    @Override
    public Object getContextAttr(String name) {
        return JWebUtils.getContextAttribute(request, name);
    }

    @Override
    public String getHeader(String name) {
        return request.getHeader(name);
    }

    @Override
    public HttpServletRequest getRequest() {
        return request;
    }

    @Override
    public HttpServletResponse getResponse() {
        return response;
    }

    @Override
    public HttpSession getSession() {
        return request.getSession();
    }

    @Override
    public HttpSession getSession(boolean create) {
        return request.getSession(create);
    }

    @Override
    public ServletContext getContext() {
        return this.getSession().getServletContext();
    }

    @Override
    public byte[] getRequestData() throws IOException {
        return JIOUtils.copyToByteArray(this.request.getInputStream());
    }

    @Override
    public String getRequestBody() throws IOException {
        String charset = JWebUtils.getRequestEncoding(request);
        return getRequestBody(charset);
    }

    @Override
    public String getRequestBody(String charset) throws IOException {
        charset = charset == null ? JWebUtils.getRequestEncoding(request) : charset;
        return JIOUtils.copyToString(this.request.getInputStream(), Charset.forName(charset));
    }

    @Override
    public <T> T getRequestJson(Class<T> clazz) throws IOException {
        return JRequestResolverManager.getBeanFromBodyWithJson(clazz, this);
    }

    @Override
    public <T> T getRequestJson(Class<T> clazz, String charset) {
        return JRequestResolverManager.getBeanFromBodyWithJson(clazz, this, charset);
    }

    @Override
    public <T> T getRequestXml(Class<T> clazz) throws IOException {
        return JRequestResolverManager.getBeanFromBodyWithXml(clazz, this);
    }

    @Override
    public <T> T getRequestXml(Class<T> clazz, String charset) {
        return JRequestResolverManager.getBeanFromBodyWithXml(clazz, this, charset);
    }

    @Override
    public void renderText(String txt) throws IOException, ServletException {
        JRenderManager.renderText(this, txt);
    }

    @Override
    public void renderDispatcher(String path) throws IOException, ServletException {
        JRenderManager.renderRequestDispatcherRender(this, path);
    }

    @Override
    public void renderRedirect(String path) throws IOException, ServletException {
        JRenderManager.renderRedirect(this, new JRedirectRenderParam(path));
    }

    @Override
    public void renderError(int code) throws IOException, ServletException {
        if (code == 404) {
            JRenderManager.renderError(this, JErrorRender.ERROR_404);
        } else if (code == 500) {
            JRenderManager.renderError(this, JErrorRender.ERROR_500);
        } else {
            JRenderManager.renderError(this, new JErrorRenderParam(code));
        }
    }

    @Override
    public void renderJson(Object json) throws IOException, ServletException {
        JRenderManager.renderJson(this, json);
    }

    @Override
    public void renderXml(Object xml) throws IOException, ServletException {
        JRenderManager.renderXml(this, xml);
    }

    @Override
    public JMultiValueMap<String, JMultipartFile> getMultipartFiles() {
        ensureParseMultipart(null);
        return this.multipartFiles;
    }

    @Override
    public JMultipartFile getFile(String name) {
        return getMultipartFiles().getFirst(name);
    }

    @Override
    public List<JMultipartFile> getFiles(String name) {
        List<JMultipartFile> multipartFiles = getMultipartFiles().get(name);
        if (multipartFiles != null) {
            return multipartFiles;
        } else {
            return Collections.emptyList();
        }
    }

    @Override
    public void cleanupMultipart() {
        JMultipartResolverFactory.cleanupFileItems(multipartFiles);
    }

    @Override
    public void parseMultipart(JMultipartResolverConfig config) {
        ensureParseMultipart(config);
    }

    private void ensureParseMultipart(JMultipartResolverConfig config) {
        if (multipartFiles == null) {
            synchronized (this) {
                if (multipartFiles == null) {
                    if (config == null) {
                        config = JMultipartResolverFactory.getMultipartResolverConfig();
                    }
                    JMultipartParsingResult result = JMultipartResolverFactory.getMultipartResolver(config)
                            .resolveMultipart(request);
                    JMultiValueMap<String, JMultipartFile> multipartFiles = null;
                    if (result != null) {
                        multipartFiles = result.getMultipartFiles();
                        Map<String, String[]> multipartParameters = result.getMultipartParameters();
                        if (request.getParameterMap().isEmpty()) {
                            request.setParameterMap(Collections.unmodifiableMap(multipartParameters));
                        } else {
                            Map<String, Object> paraMap = new HashMap<String, Object>();
                            paraMap.putAll(request.getParameterMap());
                            paraMap.putAll(multipartParameters);
                            request.setParameterMap(Collections.unmodifiableMap(paraMap));
                        }
                    } else {
                        multipartFiles = new JDefaultMultiValueMap<String, JMultipartFile>();
                    }
                    this.multipartFiles = new JDefaultMultiValueMap<String, JMultipartFile>(
                            Collections.unmodifiableMap(multipartFiles));
                }
            }
        }
    }

    @Override
    public JModuleContext getModuleContext() {
        return context;
    }
}
